package com.gitee.wsl.time.schedule.cron

import com.gitee.wsl.ext.base.currentDateTime
import com.gitee.wsl.lang.bits.BitSet
import com.gitee.wsl.time.EPOCH
import com.gitee.wsl.time.YearMonth
import com.gitee.wsl.time.numberOfDays
import com.gitee.wsl.time.schedule.AbstractTimeScheduler
import kotlin.time.Instant
import kotlinx.datetime.LocalDate
import kotlinx.datetime.LocalDateTime
import kotlinx.datetime.TimeZone
import kotlinx.datetime.number
import kotlinx.datetime.toInstant
import kotlinx.datetime.toLocalDateTime
import kotlin.time.Duration.Companion.seconds
import kotlin.time.ExperimentalTime


/**
 * Schedule class represents a parsed crontab expression.
 *
 *
 *
 * The schedule class cannot be instantiated using a constructor, a Schedule
 * object can be obtain by using the static [.create] method, which parses
 * a crontab expression and creates a Schedule object.
 *
 *
 * Original version [https://github.com/asahaf/javacron](https://github.com/asahaf/javacron)
 *
 * @author Ahmed AlSahaf
 * @author Ronald Dehuysser (minor modifications)
 */
class CronExpression private constructor() : AbstractTimeScheduler() {

    private enum class DaysAndDaysOfWeekRelation {
        INTERSECT, UNION
    }

    var expression: String? = null
        private set

    private var hasSecondsField = false
    private var daysAndDaysOfWeekRelation: DaysAndDaysOfWeekRelation? = null
    private var seconds: BitSet? = null
    private var minutes: BitSet? = null
    private var hours: BitSet? = null
    private var days: BitSet? = null
    private var months: BitSet? = null
    private var daysOfWeek: BitSet? = null
    private var daysOf5Weeks: BitSet? = null
    private var isLastDayOfMonth = false
    private var isSpecificLastDayOfMonth = false

    /**
     * Calculates the next occurrence based on provided base time.
     *
     * @param createdAtInstant Instant object based on which calculating the next occurrence.
     * @return Instant of the next occurrence.
     */
    @OptIn(ExperimentalTime::class)
    override fun next(
        createdAtInstant: Instant,
        currentInstant: Instant,
        zoneId: TimeZone
    ): Result<Instant> {

        val baseDate: LocalDateTime = currentInstant.toLocalDateTime(zoneId)

        val baseSecond: Int = baseDate.second
        val baseMinute: Int = baseDate.minute
        val baseHour: Int = baseDate.hour
        val baseDay: Int = baseDate.dayOfMonth
        val baseMonth: Int = baseDate.month.number
        val baseYear: Int = baseDate.year

        var second = baseSecond
        var minute = baseMinute
        var hour = baseHour
        var day = baseDay
        var month = baseMonth
        var year = baseYear

        if (this.hasSecondsField) {
            second++
            second = seconds!!.nextSetBit(second)
            if (second < 0) {
                second = seconds!!.nextSetBit(0)
                minute++
            }
        } else {
            minute++
        }

        minute = minutes!!.nextSetBit(minute)
        if (minute < 0) {
            hour++
            second = seconds!!.nextSetBit(0)
            minute = minutes!!.nextSetBit(0)
        } else if (minute > baseMinute) {
            second = seconds!!.nextSetBit(0)
        }

        hour = hours!!.nextSetBit(hour)
        if (hour < 0) {
            day++
            second = seconds!!.nextSetBit(0)
            minute = minutes!!.nextSetBit(0)
            hour = hours!!.nextSetBit(0)
        } else if (hour > baseHour) {
            second = seconds!!.nextSetBit(0)
            minute = minutes!!.nextSetBit(0)
        }

        var candidateDay: Int
        var candidateMonth: Int
        while (true) {
            candidateMonth = months!!.nextSetBit(month)
            if (candidateMonth < 0) {
                year++
                second = seconds!!.nextSetBit(0)
                minute = minutes!!.nextSetBit(0)
                hour = hours!!.nextSetBit(0)
                day = 1
                candidateMonth = months!!.nextSetBit(0)
            } else if (candidateMonth > month) {
                second = seconds!!.nextSetBit(0)
                minute = minutes!!.nextSetBit(0)
                hour = hours!!.nextSetBit(0)
                day = 1
            }
            month = candidateMonth
            val adjustedDaysSet: BitSet = getUpdatedDays(year, month)
            candidateDay = adjustedDaysSet.nextSetBit(day - 1) + 1
            if (candidateDay < 1) {
                month++
                second = seconds!!.nextSetBit(0)
                minute = minutes!!.nextSetBit(0)
                hour = hours!!.nextSetBit(0)
                day = 1
                continue
            } else if (candidateDay > day) {
                second = seconds!!.nextSetBit(0)
                minute = minutes!!.nextSetBit(0)
                hour = hours!!.nextSetBit(0)
            }
            day = candidateDay
            val next = LocalDateTime(year, month, day, hour, minute, second).toInstant(zoneId)
            return Result.success(next)
        }
    }

    /**
     * Compares this object against the specified object. The result is `true`
     * if and only if the argument is not `null` and is a `Schedule`
     * object that whose seconds, minutes, hours, days, months, and days of
     * weeks sets are equal to those of this schedule.
     *
     *
     * The expression string used to create the schedule is not considered, as two
     * different expressions may produce same schedules.
     *
     * @param other the object to compare with
     * @return `true` if the objects are the same; `false` otherwise
     */
    override fun equals(other: Any?): Boolean {
        if (other !is CronExpression) return false
        if (this === other) return true

        val cronExpression = other
        return this.seconds == cronExpression.seconds && (this.minutes == cronExpression.minutes) && (this.hours == cronExpression.hours) && (this.days == cronExpression.days) && (this.months == cronExpression.months) && (this.daysOfWeek == cronExpression.daysOfWeek)
    }

    override fun hashCode(): Int {
        var result: Int = seconds.hashCode()
        result = 31 * result + minutes.hashCode()
        result = 31 * result + hours.hashCode()
        result = 31 * result + days.hashCode()
        result = 31 * result + months.hashCode()
        result = 31 * result + daysOfWeek.hashCode()
        return result
    }

    val numberOfFields: Int
        get() = if (hasSecondsField) 6 else 5

    @OptIn(ExperimentalTime::class)
    private fun canScheduleActuallyOccur(): Boolean {
        if (this.daysAndDaysOfWeekRelation == DaysAndDaysOfWeekRelation.UNION || days!!.nextSetBit(0) < 29) return true

        val aYear: Int = currentDateTime.year
        for (dayIndex in 29..30) {
            if (!days!!.get(dayIndex)) continue

            for (monthIndex in 0..12) {
                if (!months!!.get(monthIndex)) continue

                if (dayIndex + 1 <= YearMonth(aYear, monthIndex).numberOfDays)
                    return true
            }
        }
        return false
    }

    @OptIn(ExperimentalTime::class)
    private fun getUpdatedDays(year: Int, month: Int): BitSet {
        val date = LocalDate(year, month, 1)
        val daysOf5WeeksOffset: Int = date.dayOfWeek.ordinal
        val updatedDays: BitSet = BitSet(31)
        updatedDays.or(this.days!!)
        val monthDaysOfWeeks: BitSet = daysOf5Weeks!!.get(daysOf5WeeksOffset, daysOf5WeeksOffset + 31)
        if (this.isSpecificLastDayOfMonth || this.daysAndDaysOfWeekRelation == DaysAndDaysOfWeekRelation.INTERSECT) {
            updatedDays.and(monthDaysOfWeeks)
        } else {
            updatedDays.or(monthDaysOfWeeks)
        }
        val dayCountInMonth: Int = YearMonth(year, month).numberOfDays
        /*if (month == Month.FEBRUARY.getValue() *//* Feb *//*) {
            dayCountInMonth = 28
            if (isLeapYear(year)) {
                dayCountInMonth++
            }
        } else {
            // We cannot use lengthOfMonth method with the month Feb
            // because it returns incorrect number of days for years
            // that are dividable by 400 like the year 2000, a bug??
            dayCountInMonth = YearMonth(year, month).days
        }*/
        // remove days beyond month length

        for (j in dayCountInMonth..30) {
            updatedDays.set(j, false)
        }

        if (isLastDayOfMonth) {
            for (j in 0 until dayCountInMonth) { // remove all days except last day of month
                updatedDays.set(j, ((j + 1) == dayCountInMonth))
            }
        } else if (isSpecificLastDayOfMonth) { // remove days before the last 7 days
            for (j in 0 until dayCountInMonth - 7) {
                updatedDays.set(j, false)
            }
        }
        return updatedDays
    }

    @OptIn(ExperimentalTime::class)
    override fun validateSchedule() {
        val base: Instant = Instant.EPOCH
        val fiveSeconds: Instant = base + (SMALLEST_SCHEDULE_IN_SECONDS).seconds

        if (next(base, base, TimeZone.UTC).getOrThrow() < fiveSeconds) {
            throw IllegalArgumentException(
                "The smallest interval for recurring jobs is $SMALLEST_SCHEDULE_IN_SECONDS seconds. Please also make sure that your 'pollIntervalInSeconds' configuration matches the smallest recurring job interval."
            )
        }
    }

    override fun toString(): String {
        return expression!!
    }

    companion object {
        private val SECONDS_FIELD_PARSER: CronFieldParser = CronFieldParser(CronFieldType.SECOND)

        private val MINUTES_FIELD_PARSER: CronFieldParser = CronFieldParser(CronFieldType.MINUTE)
        private val HOURS_FIELD_PARSER: CronFieldParser = CronFieldParser(CronFieldType.HOUR)
        private val DAYS_FIELD_PARSER: CronFieldParser = CronFieldParser(CronFieldType.DAY)
        private val MONTHS_FIELD_PARSER: CronFieldParser = CronFieldParser(CronFieldType.MONTH)
        private val DAY_OF_WEEK_FIELD_PARSER: CronFieldParser =
            CronFieldParser(CronFieldType.DAY_OF_WEEK)

        /**
         * Parses crontab expression and create a Schedule object representing that
         * expression.
         *
         *
         * The expression string can be 5 fields expression for minutes resolution.
         *
         * <pre>
         * ┌───────────── minute (0 - 59)
         * │ ┌───────────── hour (0 - 23)
         * │ │ ┌───────────── day of the month (1 - 31) or L for last day of the month
         * │ │ │ ┌───────────── month (1 - 12 or Jan/January - Dec/December)
         * │ │ │ │ ┌───────────── day of the week (0 - 6 or Sun/Sunday - Sat/Saturday)
         * │ │ │ │ │
         * │ │ │ │ │
         * │ │ │ │ │
         * "* * * * *"
        </pre> *
         *
         *
         * or 6 fields expression for higher, seconds resolution.
         *
         * <pre>
         * ┌───────────── second (0 - 59)
         * │ ┌───────────── minute (0 - 59)
         * │ │ ┌───────────── hour (0 - 23)
         * │ │ │ ┌───────────── day of the month (1 - 31) or L for last day of the month
         * │ │ │ │ ┌───────────── month (1 - 12 or Jan/January - Dec/December)
         * │ │ │ │ │ ┌───────────── day of the week (0 - 6 or Sun/Sunday - Sat/Saturday)
         * │ │ │ │ │ │
         * │ │ │ │ │ │
         * │ │ │ │ │ │
         * "* * * * * *"
        </pre> *
         *
         * @param expression a crontab expression string used to create Schedule.
         * @return Schedule object created based on the supplied crontab expression.
         * @throws InvalidCronExpressionException if the provided crontab expression is
         * invalid. The crontab expression is
         * considered invalid if it is not properly
         * formed, like empty string or contains less
         * than 5 fields or more than 6 field. It's
         * also invalid if the values in a field are
         * beyond the allowed values range of that
         * field. Non-occurring schedules like "0 0
         * 30 2 *" is considered invalid too, as Feb
         * never has 30 days and a schedule like this
         * never occurs.
         */
        fun create(expression: String): CronExpression {
            if (expression.isEmpty()) {
                throw InvalidCronExpressionException("empty expression")
            }
            val fields = expression.trim { it <= ' ' }.lowercase()
                .split("\\s+".toRegex()).dropLastWhile { it.isEmpty() }
                .toTypedArray()
            val count = fields.size
            if (count > 6 || count < 5) {
                throw InvalidCronExpressionException(
                    "crontab expression should have 6 fields for (seconds resolution) or 5 fields for (minutes resolution)"
                )
            }
            val cronExpression = CronExpression()
            cronExpression.hasSecondsField = count == 6
            var token: String
            var index = 0
            if (cronExpression.hasSecondsField) {
                token = fields[index++]
                cronExpression.seconds = SECONDS_FIELD_PARSER.parse(token)
            } else {
                cronExpression.seconds = BitSet(1)
                cronExpression.seconds!!.set(0)
            }
            token = fields[index++]
            cronExpression.minutes = MINUTES_FIELD_PARSER.parse(token)

            token = fields[index++]
            cronExpression.hours = HOURS_FIELD_PARSER.parse(token)

            token = fields[index++]
            val daysToken = token
            cronExpression.days = DAYS_FIELD_PARSER.parse(token)
            cronExpression.isLastDayOfMonth = token == "l"
            val daysStartWithAsterisk = token.startsWith("*")

            token = fields[index++]
            cronExpression.months = MONTHS_FIELD_PARSER.parse(token)

            token = fields[index++]
            cronExpression.daysOfWeek = DAY_OF_WEEK_FIELD_PARSER.parse(token)
            val daysOfWeekStartAsterisk = token.startsWith("*")

            if (token.length == 2 && token.endsWith("l")) {
                if (cronExpression.isLastDayOfMonth) {
                    throw InvalidCronExpressionException("You can only specify the last day of month week in either the DAY field or in the DAY_OF_WEEK field, not both.")
                }
                if (!daysToken.equals("*", ignoreCase = true)) {
                    throw InvalidCronExpressionException("when last days of month is specified. the day of the month must be \"*\"")
                }
                // this flag will be used later duing finding the next schedule as some months have less than 31 days
                cronExpression.isSpecificLastDayOfMonth = true
            }
            cronExpression.daysOf5Weeks = generateDaysOf5Weeks(cronExpression.daysOfWeek!!)

            cronExpression.daysAndDaysOfWeekRelation =
                if ((daysStartWithAsterisk || daysOfWeekStartAsterisk)
                ) DaysAndDaysOfWeekRelation.INTERSECT
                else DaysAndDaysOfWeekRelation.UNION

            if (!cronExpression.canScheduleActuallyOccur()) throw InvalidCronExpressionException("Cron expression not valid. The specified months do not have the day 30th or the day 31st")
            cronExpression.expression = expression.trim { it <= ' ' }
            return cronExpression
        }

        /*fun isLeapYear(year: Int): Boolean {
            val cal: java.util.Calendar = java.util.Calendar.getInstance()
            cal.set(java.util.Calendar.YEAR, year)
            return cal.getActualMaximum(java.util.Calendar.DAY_OF_YEAR) > 365
        }*/

        private fun generateDaysOf5Weeks(daysOfWeek: BitSet): BitSet {
            val weekLength = 7
            val setLength = weekLength + 31
            val bitSet: BitSet = BitSet(setLength)
            var i = 0
            while (i < setLength) {
                for (j in 0 until weekLength) {
                    bitSet[j + i] = daysOfWeek[j]
                }
                i += weekLength
            }
            return bitSet
        }
    }
}